-
-
Notifications
You must be signed in to change notification settings - Fork 33.8k
fix abortsignal: make AbortSignal.timeout mock-timers compatible #60587
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix abortsignal: make AbortSignal.timeout mock-timers compatible #60587
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DOM standard does not use setTimeout for this method, and changes to the public-facing timers API should not be observable here.
The correct route would probably be to add this as a discrete API target for the mocker (ie. enabled via mocks.timers.enable({ apis: ['AbortSignal.timeout'] })) with its own implementation.
|
would you also add tests for this feat? |
|
Thanks a lot for the guidance! |
Renegade334
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this! There are a few changes to make, but this is definitely heading in the right direction.
| #storeOriginalAbortSignalTimeout() { | ||
| try { | ||
| this.#realAbortSignalTimeout = ObjectGetOwnPropertyDescriptor(AbortSignal, 'timeout'); | ||
| } catch { | ||
| this.#realAbortSignalTimeout = undefined; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The store/restore methods should not be allowed to fail silently.
| #storeOriginalAbortSignalTimeout() { | |
| try { | |
| this.#realAbortSignalTimeout = ObjectGetOwnPropertyDescriptor(AbortSignal, 'timeout'); | |
| } catch { | |
| this.#realAbortSignalTimeout = undefined; | |
| } | |
| } | |
| #storeOriginalAbortSignalTimeout() { | |
| this.#realAbortSignalTimeout = ObjectGetOwnPropertyDescriptor(AbortSignal, 'timeout'); | |
| } |
| #restoreOriginalAbortSignalTimeout() { | ||
| try { | ||
| if (this.#realAbortSignalTimeout) { | ||
| ObjectDefineProperty(AbortSignal, 'timeout', this.#realAbortSignalTimeout); | ||
| } else { | ||
| try { | ||
| delete AbortSignal.timeout; | ||
| } catch {} | ||
| } | ||
| } catch {} | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| #restoreOriginalAbortSignalTimeout() { | |
| try { | |
| if (this.#realAbortSignalTimeout) { | |
| ObjectDefineProperty(AbortSignal, 'timeout', this.#realAbortSignalTimeout); | |
| } else { | |
| try { | |
| delete AbortSignal.timeout; | |
| } catch {} | |
| } | |
| } catch {} | |
| } | |
| #restoreOriginalAbortSignalTimeout() { | |
| ObjectDefineProperty(AbortSignal, 'timeout', this.#realAbortSignalTimeout); | |
| } |
true
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please delete this artifact!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how 0694adb made it into your branch. You will need to drop this commit, using interactive rebase for example.
| /** | ||
| * Advances the virtual time of MockTimers by the specified duration (in milliseconds). | ||
| * This method simulates the passage of time and triggers any scheduled timers that are due. | ||
| * @param {number} [time] - The amount of time (in milliseconds) to advance the virtual time. | ||
| * @throws {ERR_INVALID_STATE} If MockTimers are not enabled. | ||
| * @throws {ERR_INVALID_ARG_VALUE} If a negative time value is provided. | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why these are being removed – unintentional AI action perhaps? Please restore these.
| /** | ||
| * @typedef {{apis: SUPPORTED_APIS;now: number | Date;}} EnableOptions Options to enable the timers | ||
| * @property {SUPPORTED_APIS} apis List of timers to enable, defaults to all | ||
| * @property {number | Date} now The epoch to which the timers should be set to, defaults to 0 | ||
| */ | ||
| /** | ||
| * Enables the MockTimers replacing the native timers with the fake ones. | ||
| * @param {EnableOptions} [options] | ||
| */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
| internalOptions.apis ||= SUPPORTED_APIS; | ||
|
|
||
| validateStringArray(internalOptions.apis, 'options.apis'); | ||
| // Check that the timers passed are supported |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
lib/internal/abort_controller.js
Outdated
| clearTimeout, | ||
| setTimeout, | ||
| } = require('timers'); | ||
| clearTimeout: internalClearTimeout, | ||
| setTimeout: internalSetTimeout, | ||
| } = require('internal/timers'); | ||
|
|
||
| const clearTimeout = globalThis.clearTimeout; | ||
| const setTimeout = globalThis.setTimeout; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please revert the original changes to lib/internal/abort_controller.
| 'AbortSignal.timeout': () => { | ||
| this.#storeOriginalAbortSignalTimeout(); | ||
|
|
||
| const mock = this; | ||
|
|
||
| ObjectDefineProperty(AbortSignal, 'timeout', { | ||
| __proto__: null, | ||
| configurable: true, | ||
| writable: true, | ||
| value: function mockableAbortSignalTimeout(delay) { | ||
| if (NumberIsNaN(delay)) { | ||
| throw new ERR_INVALID_ARG_VALUE('delay', delay, 'delay must be a number'); | ||
| } | ||
|
|
||
| const controller = new AbortController(); | ||
|
|
||
| const timer = mock.#setTimeout( | ||
| () => { | ||
| try { | ||
| controller.abort(); | ||
| } catch {} | ||
| }, | ||
| delay | ||
| ); | ||
|
|
||
| try { | ||
| ObjectDefineProperty(controller.signal, '__mockTimer', { | ||
| __proto__: null, | ||
| configurable: true, | ||
| writable: true, | ||
| value: timer, | ||
| }); | ||
| } catch {} | ||
|
|
||
| return controller.signal; | ||
| }, | ||
| }); | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, nothing here should be able to fail silently; please remove the try/catch blocks.
| try { | ||
| ObjectDefineProperty(controller.signal, '__mockTimer', { | ||
| __proto__: null, | ||
| configurable: true, | ||
| writable: true, | ||
| value: timer, | ||
| }); | ||
| } catch {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think there's any need to define this; no underlying timer is exposed on true AbortSignals, and the timeout is protected from GC while it remains active in the execution queue.
c30dce3 to
735ee27
Compare
Fixes: #60509
AbortSignal.timeout()previously usedinternal/timers.setTimeout, which was not intercepted bymock.timers.enable().This change updates it to use
globalThis.setTimeoutandglobalThis.clearTimeout, somock.timers.tick()correctly triggers aborts./cc @nodejs/testing
Notes for reviewers
This change replaces
require('internal/timers')usage with global timers,so
mock.timersin the Node test runner can interceptAbortSignal.timeout.Verified locally that the fix aligns with issue reproduction steps.